/****************************************************************************
 * Copyright (c) 2004 Composent, Inc. and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *    Composent, Inc. - initial API and implementation
 *
 * SPDX-License-Identifier: EPL-2.0
 *****************************************************************************/

package org.eclipse.ecf.example.collab.share;

import java.util.Hashtable;
import java.util.Vector;

import org.eclipse.ecf.core.identity.ID;
import org.eclipse.ecf.core.sharedobject.ISharedObjectContainerTransaction;
import org.eclipse.ecf.core.sharedobject.SharedObjectAddAbortException;
import org.eclipse.ecf.internal.example.collab.Messages;

public class TransactionSharedObject extends GenericSharedObject implements
		ISharedObjectContainerTransaction {

	public static final String REPLICA_COMMIT_MSG = "replicaCommit"; //$NON-NLS-1$

	public static int DEFAULT_TIMEOUT = 30000;

	// Dummy inner class to provide lock
	static final class Lock {
	}

	// Timeout value associated with this object's replication
	protected int timeout;
	// Replication state this object is currently in.
	protected byte state;
	// A lock variable
	protected Lock lock;
	protected Vector participantIDs;
	protected Hashtable failedParticipants;

	public TransactionSharedObject(int timeout) {
		this.timeout = timeout;
		init();
	}

	public TransactionSharedObject() {
		this(DEFAULT_TIMEOUT);
	}

	protected void init() {
		state = ISharedObjectContainerTransaction.ACTIVE;
		lock = new Lock();
		participantIDs = new Vector();
		failedParticipants = new Hashtable();
	}

	public void activated(ID[] others) {
		// No other state changes while this is going on
		synchronized (lock) {
			if (isHost()) {
				replicate(null);
				addRemoteParticipants(getContext().getGroupMemberIDs());
				state = ISharedObjectContainerTransaction.VOTING;
				// Clients
			} else {
				try {
					// Try to respond with create success message back to host
					getContext().sendCreateResponse(getHomeContainerID(), null,
							getNextReplicateID());
					// If above succeeds, we're now in prepared state
					state = ISharedObjectContainerTransaction.PREPARED;
				} catch (Exception e) {
					// If throws exception, we're doomed
					state = ISharedObjectContainerTransaction.ABORTED;
					log("unable to send create response to " //$NON-NLS-1$
							+ getHomeContainerID(), e);
				}
			}
			// Notify any threads waiting on state change
			lock.notifyAll();
		}
	}

	public void memberAdded(ID member) {
		if (isHost()) {
			// If we are currently in VOTING state, then add the new member to
			// list of participants
			// and send replicate message. If not in voting state, just send
			// replicate message
			synchronized (lock) {
				replicate(member);
				if (getTransactionState() == ISharedObjectContainerTransaction.VOTING)
					addRemoteParticipants(new ID[] { member });
				else
					replicate(member);
			}
		}
	}

	protected void addRemoteParticipants(ID ids[]) {
		if (ids != null && participantIDs != null) {
			for (int i = 0; i < ids.length; i++) {
				if (!getHomeContainerID().equals(ids[i]))
					participantIDs.addElement(ids[i]);
			}
		}
	}

	protected void removeRemoteParticipant(ID id) {
		if (id != null && participantIDs != null) {
			int index = participantIDs.indexOf(id);
			if (index != -1)
				participantIDs.removeElementAt(index);
		}
	}

	protected void addRemoteParticipantFailed(ID remote, Throwable failure) {
		if (remote != null && failure != null && failedParticipants != null) {
			failedParticipants.put(remote, failure);
		}
	}

	public void handleCreateResponse(ID fromID, Throwable e, Long identifier) {
		// If no exception, remove
		synchronized (lock) {
			if (state == ISharedObjectContainerTransaction.VOTING) {
				if (e == null) {
					removeRemoteParticipant(fromID);
				} else {
					addRemoteParticipantFailed(fromID, e);
				}
			} else {
				handleVotingCompletedCreateResponse(fromID, e, identifier);
			}
			lock.notifyAll();
		}
	}

	protected void handleVotingCompletedCreateResponse(ID fromID, Throwable e,
			Long identifier) {
		// If remote creation was successful, simply send commit message back.
		if (e == null) {
			// send commit message right back.
			try {
				forwardMsgTo(fromID, SharedObjectMsg.createMsg((String) null,
						REPLICA_COMMIT_MSG));
			} catch (Exception except) {
				log("Exception sending commit message to " + fromID, except); //$NON-NLS-1$
			}
		}
	}

	public void memberRemoved(ID member) {
		// We only care about this if we are the host.
		if (isHost()) {
			synchronized (lock) {
				if (state == ISharedObjectContainerTransaction.VOTING) {
					addRemoteParticipantFailed(member, new Exception("Member " //$NON-NLS-1$
							+ member + " left")); //$NON-NLS-1$
				}
				lock.notifyAll();
			}
		}
	}

	public void waitToCommit() throws SharedObjectAddAbortException {
		synchronized (lock) {
			long end = System.currentTimeMillis() + timeout;
			try {
				while (!votingCompleted()) {
					long wait = end - System.currentTimeMillis();
					if (wait <= 0L)
						throw new SharedObjectAddAbortException(
								Messages.TransactionSharedObject_EXCEPTION_TIMEOUT);
					// Actually wait right here
					lock.wait(wait);
				}
			} catch (InterruptedException e) {
				throw new SharedObjectAddAbortException(Messages.TransactionSharedObject_EXCEPTION_INTERUPTED);
			} catch (SharedObjectAddAbortException e1) {
				// Aborted for some reason. Clean up.
				doAbort(e1);
			}
			// Success. Send commit to remotes and clean up before returning.
			doCommit();
		}
	}

	public byte getTransactionState() {
		synchronized (lock) {
			return state;
		}
	}

	protected void doAbort(SharedObjectAddAbortException e)
			throws SharedObjectAddAbortException {
		// Send destroy message here so all remotes get destroyed, and we remove
		// ourselves from local space as well.
		destroySelf();
		// Set our own state variable to ABORTED
		state = ISharedObjectContainerTransaction.ABORTED;
		// throw so caller gets exception and can deal with it
		throw e;
	}

	public void doCommit() throws SharedObjectAddAbortException {
		// Get current membership
		int others = 0;
		others = getContext().getGroupMemberIDs().length;
		// Only forward commit message if the participantIDs array is not yet
		// null,
		// and the current membership is > 0 (we're connected to something)
		if (participantIDs != null && others > 0) {
			// Send replicaCommit message to all remote clients
			try {
				forwardMsgTo(null, SharedObjectMsg.createMsg((String) null,
						REPLICA_COMMIT_MSG));
			} catch (Exception e2) {
				doAbort(new SharedObjectAddAbortException(
						Messages.TransactionSharedObject_EXCEPTION_ON_COMMIT_MESSAGE, e2));
			}
		}
		// Set state variable to committed.
		state = ISharedObjectContainerTransaction.COMMITTED;
		// Call local committed message
		committed();
		participantIDs = null;
		failedParticipants = null;
	}

	protected void execMsgInvoke(SharedObjectMsg msg, ID fromID, Object o)
			throws Exception {
		if (o == this) {
			// Object[] args = msg.getArgs();
			String name = msg.getMethodName();
			if (name.equals(REPLICA_COMMIT_MSG)) {
				replicaCommit();
				return;
			}
		}
		super.execMsgInvoke(msg, fromID, o);
	}

	public final void replicaCommit() {
		synchronized (lock) {
			state = COMMITTED;
			lock.notifyAll();
			participantIDs = null;
			failedParticipants = null;
		}
		// Call subclass overrideable method
		committed();
	}

	protected void committed() {
		// Subclasses may override as appropriate
	}

	protected boolean votingCompleted() throws SharedObjectAddAbortException {
		// The test here is is we've received any indication of failed
		// participants in
		// the transaction. If so, we throw.
		if (failedParticipants != null && failedParticipants.size() > 0) {
			ID remoteID = (ID) failedParticipants.keys().nextElement();
			Exception e = (Exception) failedParticipants.get(remoteID);
			// Abort!
			throw new SharedObjectAddAbortException(Messages.TransactionSharedObject_EXCEPTION_FROM_ABORT, e);
			// If no problems, and the number of participants to here from is 0,
			// then we're done
		} else if (state == ISharedObjectContainerTransaction.VOTING
				&& participantIDs.size() == 0) {
			// Success!
			return true;
		}
		// Else continue waiting
		return false;
	}
}
